﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Runtime.Caching;
using System.Timers;
using TeamCityMonitor.Models;
using TeamCitySharp;
using TeamCitySharp.DomainEntities;
using TeamCitySharp.Locators;

namespace TeamCityMonitor.Repository.Caches
{
    public interface ITeamCityCacheRefresher
    {
    }

    public class TeamCityCacheRefresher : ITeamCityCacheRefresher
    {
        private static TeamCityClient client;

        private static Timer refreshBaseDataCacheTimer;
        private static Timer refreshLastestBuildsCacheTimer;
        private static Timer refreshBuildRunningCacheTimer;
        private static Timer refreshStatisticsCacheTimer;

        private static ObjectCache cache;

        public static TeamCityDataCacheObject DataCache
        {
            get { return cache["teamcity_data"] as TeamCityDataCacheObject; }
        }

        public static TeamCityStatisticsCacheObject StatisticsCache
        {
            get { return cache["teamcity_stats"] as TeamCityStatisticsCacheObject; }
        }

        private static List<Build> LastRunningBuilds;

        static TeamCityCacheRefresher()
        {
            client = new TeamCityClient(Properties.Settings.Default.TeamCityURL, false);
            client.Connect(
                              Properties.Settings.Default.TeamCityUser,
                              Properties.Settings.Default.TeamCityPwd,
                              false
                          );

            cache = MemoryCache.Default;
            cache["teamcity_data"] = new TeamCityDataCacheObject();
            cache["teamcity_stats"] = new TeamCityStatisticsCacheObject();
            LastRunningBuilds = new List<Build>();

            refreshBaseDataCacheTimer = new Timer(1000);
            refreshBaseDataCacheTimer.Elapsed += RefreshBaseDataCacheTimerOnElapsed;
            refreshBaseDataCacheTimer.Start();

            refreshLastestBuildsCacheTimer = new Timer(10000);
            refreshLastestBuildsCacheTimer.Elapsed += RefreshLastestBuildsCacheTimerOnElapsed;
            refreshLastestBuildsCacheTimer.Start();

            refreshBuildRunningCacheTimer = new Timer(Properties.Settings.Default.RefreshRunningStatusCacheInterval);
            refreshBuildRunningCacheTimer.Elapsed += RefreshBuildRunningCacheTimerOnElapsed;
            refreshBuildRunningCacheTimer.Start();

            refreshStatisticsCacheTimer = new Timer(30000);
            refreshStatisticsCacheTimer.Elapsed += RefreshStatisticsCacheTimerOnElapsed;
            refreshStatisticsCacheTimer.Start();
        }

        public static void Dispose()
        {
            if (refreshBaseDataCacheTimer != null) refreshBaseDataCacheTimer.Stop();
            if (refreshLastestBuildsCacheTimer != null) refreshLastestBuildsCacheTimer.Stop();
            if (refreshBuildRunningCacheTimer != null) refreshBuildRunningCacheTimer.Stop();
            if (refreshStatisticsCacheTimer != null) refreshStatisticsCacheTimer.Stop();
            client = null;
        }

        private static void UpdateData(Timer timer, int interval, string logger, Action action)
        {
            timer.Stop();
            var stopWatch = new Stopwatch();
            stopWatch.Start();
            try
            {
                action.Invoke();
            }
            catch (Exception e)
            {
                log4net.LogManager.GetLogger(logger).Error("Error updating cache.", e);
            }
            finally
            {
                stopWatch.Stop();
                UpdateCacheMeasurement(logger, stopWatch.ElapsedMilliseconds);
            }

            timer.Interval = interval;
            timer.Start();
        }

        private static void UpdateCacheMeasurement(string name, long executionTime)
        {
            try
            {
                var repository = new CacheStatisticsRepository();
                var stat = repository.GetAllStatistics().FirstOrDefault(sta => sta.Name == name) ?? new CacheStatistics();

                stat.Name = name;
                stat.NoOfExecutions += 1;
                stat.TotalExecutionTime += executionTime;
                stat.MeanExecutionTime = stat.TotalExecutionTime/stat.NoOfExecutions;

                var now = DateTime.Now;

                if (stat.MinimumExecutionTime > executionTime)
                {
                    stat.MinimumExecutionTime = executionTime;
                    stat.MinimumExecutionTimeDate = now;
                }

                if (stat.MaximumExecutionTime < executionTime)
                {
                    stat.MaximumExecutionTime = executionTime;
                    stat.MaximumExecutionTimeDate = now;
                }

                repository.Save(stat);
            }
            catch (Exception e)
            {
                log4net.LogManager.GetLogger("Cache").Error("Error storing statistics measurements.", e);
            }
        }

        private static void RefreshBaseDataCacheTimerOnElapsed(object sender, ElapsedEventArgs elapsedEventArgs)
        {
            UpdateData( refreshBaseDataCacheTimer, 
                        Properties.Settings.Default.RefreshAllBuildsCacheInterval, 
                        "BaseDataCache", 
                        () => {
                                UpdateActiveProjects();
                                UpdateActiveBuildConfigurations();
                                UpdateUsers();
                                UpdateServerInformation();
                                UpdateBuilds();
                        });
        }

        private static void RefreshLastestBuildsCacheTimerOnElapsed(object sender, ElapsedEventArgs elapsedEventArgs)
        {
            UpdateData( refreshLastestBuildsCacheTimer,
                        Properties.Settings.Default.RefreshLatestBuildsCacheInterval,
                        "LatestBuildsCache",
                        UpdateLatestBuilds);
        }

        private static void RefreshBuildRunningCacheTimerOnElapsed(object sender, ElapsedEventArgs elapsedEventArgs)
        {
            UpdateData( refreshBuildRunningCacheTimer,
                        Properties.Settings.Default.RefreshRunningStatusCacheInterval,
                        "RunningBuildsCache",
                        UpdateRunningBuildConfigIds);
        }

        private static void RefreshStatisticsCacheTimerOnElapsed(object sender, ElapsedEventArgs elapsedEventArgs)
        {
            UpdateData(refreshStatisticsCacheTimer,
            Properties.Settings.Default.RefreshStatisticsCacheInterval,
            "StatisticsCache",
            () =>
                {
                    UpdateNoOfProjects();
                    UpdateNoOfConfigurations();
                    UpdateNoOfBuilds();

                    UpdateNoOfBuildSince();

                    UpdateNoOfBuildsByBuildConfig();

                    UpdateNoOfBuildsByUser();
                });
        }

        private static void UpdateActiveProjects()
        {
            DataCache.ActiveProjects = client.AllProjects()
                                            .Select(project => client.ProjectDetails(project)).ToList()
                                                                     .Where(ProjectIsNotArchived)
                                                                        .Where(ProjectHasConfigurations)
                                                                            .ToList();
        }

        private static void UpdateBuilds()
        {
            DataCache.Builds = client.BuildsByBuildLocator(BuildLocator.WithDimensions(startIndex: 0));
        }

        private static bool ProjectIsNotArchived(Project project)
        {
            return project.Archived == false;
        }

        private static bool ProjectHasConfigurations(Project project)
        {
            return project.BuildTypes != null;
        }

        private static void UpdateActiveBuildConfigurations()
        {
            var buildConfigs = new List<BuildConfig>();
            var projects = DataCache.ActiveProjects.ToList();
            projects.ForEach(project => buildConfigs.AddRange(project.BuildTypes.BuildType));
            DataCache.BuildConfigurations = buildConfigs;
        }

        private static void UpdateUsers()
        {
            DataCache.Users = client.AllUsers();
        }

        private static void UpdateServerInformation()
        {
            DataCache.ServerInformation = client.ServerInfo();
        }

        private static void UpdateRunningBuildConfigIds()
        {
            var builds = client.BuildsByBuildLocator(BuildLocator.RunningBuilds());

            DataCache.RunningBuilds = (builds != null ? builds.ToList() : new List<Build>());

            ReEvaluateBuildsWhichHaveLeftTheRunningState();
        }

        private static void ReEvaluateBuildsWhichHaveLeftTheRunningState()
        {
            var currentlyRunningBuilds = DataCache.RunningBuilds.ToList();

            var idsOfbuildsWhichAreNoLongerRunning = LastRunningBuilds.Where(bld => currentlyRunningBuilds.Any(cbld => cbld.Id == bld.Id) == false).Select(bld => bld.BuildTypeId).ToList();

            var buildsWhichAreNoLongerRunning = new List<Build>();
            DataCache.BuildConfigurations.Where(config => idsOfbuildsWhichAreNoLongerRunning.Contains(config.Id)).ToList()
                                            .ForEach(config => buildsWhichAreNoLongerRunning.Add(GetLastBuild(config)));

            DataCache.LatestBuilds.Where(lbld => buildsWhichAreNoLongerRunning.Exists(bld => bld.BuildTypeId == lbld.BuildTypeId)).ToList()
                                    .ForEach(bld =>
                                                 {
                                                     var newBld = buildsWhichAreNoLongerRunning.First(notRunningBld => notRunningBld.BuildTypeId == bld.BuildTypeId);
                                                     bld.Status = newBld.Status;
                                                     bld.StatusText = newBld.StatusText;
                                                 });

            LastRunningBuilds = currentlyRunningBuilds.ToList();
        }

        private static void UpdateLatestBuilds()
        {
            var latestBuilds = new List<Build>();

            var configs = DataCache.BuildConfigurations.ToList();
            configs.ForEach(config =>
            {
                var build = GetLastBuild(config);
                build = AttemptToGetPreviousBuildInKnownStateIfCurrenctBuildStatusIsUnknown(build);
                build.Changes = GetLatestChanges(build);
                latestBuilds.Add(build);
            });

            DataCache.LatestBuilds = latestBuilds;
        }

        private static ChangeWrapper GetLatestChanges(Build build)
        {
            var changeWrapper = new ChangeWrapper { Change = new List<Change>() };

            try
            {
                if (ShouldFetchChanges(build))
                {
                    client.ChangeDetailsByBuildConfigId(build.BuildConfig.Id)
                        .ForEach(change => changeWrapper.Change.Add(client.ChangeDetailsByChangeId(change.Id)));
                }
            }
            finally{}

            if (changeWrapper.Change.Any() == false)
            {
                changeWrapper.Change.Add(new Change() {Username = "(No change/user found)", Comment = ""});
            }

            return changeWrapper;
        }

        private static bool ShouldFetchChanges(Build build)
        {
            if (Properties.Settings.Default.ShowBuildConfigsWhichHaveNeverBeenBuild && build.Status == "NEVERBUILD")
            {
                return true;
            }

            if (Properties.Settings.Default.ShowBuildsWhichAreInAnUnknownState && build.Status == "UNKNOWN")
            {
                return true;
            }

            return false;
        }

        private static Build GetLastBuild(BuildConfig config)
        {
            var build = new Build { Status = "NEVERBUILD", BuildConfig = config};
            try
            {
                var latestBuild = client.LastBuildByBuildConfigId(config.Id);
                latestBuild.BuildConfig = config;

                if (string.IsNullOrEmpty(latestBuild.Id) == false)
                {
                    build = latestBuild;
                }
            }
            catch { }
            return build;
        }

        private static Build AttemptToGetPreviousBuildInKnownStateIfCurrenctBuildStatusIsUnknown(Build build)
        {
            var builds = new List<Build> { build };
            
            if (string.IsNullOrEmpty(build.Status) || build.Status == "UNKNOWN")
            {
                builds.Add(GetLastErrorBuild(build.BuildConfig));
                builds.Add(GetLastFailedBuild(build.BuildConfig));
                builds.Add(GetLastSuccessfullBuild(build.BuildConfig));

                builds = builds.Where(bld => bld.Status != "UNKNOWN")
                                .OrderByDescending(bld => bld.StartDate).ToList();
            }

            return builds.First();
        }

        private static Build GetLastErrorBuild(BuildConfig config)
        {
            var build = new Build { Status = "UNKNOWN" };
            try
            {
                build = client.LastErrorBuildByBuildConfigId(config.Id);
                build.BuildConfig = config;
            }
            catch { }
            return build;
        }

        private static Build GetLastFailedBuild(BuildConfig config)
        {
            var build = new Build { Id = "UNKNOWN" };
            try
            {
                build = client.LastFailedBuildByBuildConfigId(config.Id);
                build.BuildConfig = config;
            }
            catch { }
            return build;
        }

        private static Build GetLastSuccessfullBuild(BuildConfig config)
        {
            var build = new Build { Id = "UNKNOWN" };
            try
            {
                build = client.LastSuccessfulBuildByBuildConfigId(config.Id);
                build.BuildConfig = config;
            }
            catch { }
            return build;
        }

        private static void UpdateNoOfConfigurations()
        {
            StatisticsCache.NoOfProjects = DataCache.ActiveProjects.Count();
        }

        private static void UpdateNoOfProjects()
        {
            StatisticsCache.NoOfConfigurations = DataCache.BuildConfigurations.Count();
        }

        private static void UpdateNoOfBuilds()
        {
            StatisticsCache.NoOfBuilds = DataCache.Builds.Count();
            StatisticsCache.NoOfSuccessfullBuilds = DataCache.Builds.Count(bld => bld.Status == "SUCCESS");
            StatisticsCache.NoOfErrorBuilds = DataCache.Builds.Count(bld => bld.Status == "ERROR");
            StatisticsCache.NoOfFailingBuilds = DataCache.Builds.Count(bld => bld.Status == "FAILURE");
            StatisticsCache.NoOfUnknownBuilds = StatisticsCache.NoOfBuilds - StatisticsCache.NoOfSuccessfullBuilds - StatisticsCache.NoOfErrorBuilds - StatisticsCache.NoOfFailingBuilds;
        }

        private static void UpdateNoOfBuildSince()
        {
            var buildsFromCache = DataCache.Builds.ToList();

            var buildsSinceDate = buildsFromCache.Where( bld => bld.StartDate >= DateTime.Now.AddDays(-1 * Properties.Settings.Default.DaysBackToPerformStatistisc));

            StatisticsCache.NoOfBuildsSince = buildsSinceDate.Count();
            StatisticsCache.NoOfSuccessfullBuildsSince = buildsSinceDate.Count(bld => bld.Status == "SUCCESS");
            StatisticsCache.NoOfErrorBuildsSince = buildsSinceDate.Count(bld => bld.Status == "ERROR");
            StatisticsCache.NoOfFailingBuildsSince = buildsSinceDate.Count(bld => bld.Status == "FAILURE");
            StatisticsCache.NoOfUnknownBuildsSince = buildsSinceDate.Count() - StatisticsCache.NoOfSuccessfullBuildsSince - StatisticsCache.NoOfErrorBuildsSince - StatisticsCache.NoOfFailingBuildsSince;
        }

        private static void UpdateNoOfBuildsByUser()
        {
            var users = DataCache.Users.ToList();

            users.ForEach(user =>
                              {
                                  var builds = client.BuildsByUserName(user.Username);
                                  
                                  AddToDictionary(StatisticsCache.NoOfBuildsByUser, user.Id, builds.Count());
                                  AddToDictionary(StatisticsCache.NoOfSuccessfullBuildsByUser, user.Id, builds.Count(bld => bld.Status == "SUCCESS"));
                                  AddToDictionary(StatisticsCache.NoOfErrorBuildsByUser, user.Id, builds.Count(bld => bld.Status == "ERROR"));
                                  AddToDictionary(StatisticsCache.NoOfFailingBuildsByUser, user.Id, builds.Count(bld => bld.Status == "FAILURE"));
                              });
        }

        private static void UpdateNoOfBuildsByBuildConfig()
        {
            var configs = DataCache.BuildConfigurations.ToList();
            var buildsFromCache = DataCache.Builds.ToList();

            configs.ForEach(config =>
                                {
                                    var builds = buildsFromCache.Where(bld => bld.BuildTypeId == config.Id).ToList();

                                    AddToDictionary(StatisticsCache.NoOfBuildsByBuildConfig, config.Id, builds.Count());
                                    AddToDictionary(StatisticsCache.NoOfSuccessfullBuildsByBuildConfig, config.Id, builds.Count(bld => bld.Status == "SUCCESS"));
                                    AddToDictionary(StatisticsCache.NoOfErrorBuildsByBuildConfig, config.Id, builds.Count(bld => bld.Status == "ERROR"));
                                    AddToDictionary(StatisticsCache.NoOfFailingBuildsByBuildConfig, config.Id, builds.Count(bld => bld.Status == "FAILURE"));
                                });
        }


        private static void AddToDictionary(Dictionary<string, int> dict, string key, int count)
        {
            if (dict.ContainsKey(key))
            {
                dict[key] = count;
            }
            else
            {
                dict.Add(key, count);
            }
        }
    }
}